"""
    Copyright 2011 Kacper Żuk <kazuldur@gmail.com>

    This file is part of pyLastPlayer.

    pyLastPlayer is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    pyLastPlayer is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with pyLastPlayer.  If not, see <http://www.gnu.org/licenses/>.
"""

import urllib.request, urllib.parse, urllib.error, http.client
import time
from hashlib import md5
from xml.dom import minidom

# Services:
#    0 - Last.fm
#    1 - Libre.fm

errors = {
            1 : "This error does not exist",
            2 : "Invalid service -This service does not exist",
            3 : "Invalid Method - No method with that name in this package",
            4 : "Authentication Failed - You do not have permissions to access the service",
            5 : "Invalid format - This service doesn't exist in that format",
            6 : "Invalid parameters - Your request is missing a required parameter",
            7 : "Invalid resource specified",
            8 : "Operation failed - Most likely the backend service failed. Please try again.",
            9 : "Invalid session key - Please re-authenticate",
            10 : "Invalid API key - You must be granted a valid key by last.fm",
            11 : "Service Offline - This service is temporarily offline. Try again later.",
            12 : "Subscribers Only - This station is only available to paid last.fm subscribers",
            13 : "Invalid method signature supplied",
            14 : "Unauthorized Token - This token has not been authorized",
            15 : "This item is not available for streaming.",
            16 : "The service is temporarily unavailable, please try again.",
            17 : "Login: User requires to be logged in",
            18 : "Trial Expired - This user has no free radio plays left. Subscription required.",
            19 : "This error does not exist",
            20 : "Not Enough Content - There is not enough content to play this station",
            21 : "Not Enough Members - This group does not have enough members for radio",
            22 : "Not Enough Fans - This artist does not have enough fans for for radio",
            23 : "Not Enough Neighbours - There are not enough neighbours for radio",
            24 : "No Peak Radio - This user is not allowed to listen to radio during peak usage",
            25 : "Radio Not Found - Radio station not found",
            26 : "API Key Suspended - This application is not allowed to make requests to the web services",
            27 : "Deprecated - This type of request is no longer supported",
}

services = (
                ("Last.fm", "http://ws.audioscrobbler.com/2.0/?", "http://www.last.fm/api/auth/?"),
                ("Libre.fm", "http://alpha.libre.fm/2.0/?", "http://alpha.libre.fm/api/auth/?"),
            )

class Track:
    def __init__(self, id, artist, album, title, duration, location, image=None, genre=None):
        self.id = id
        self.artist = artist.replace("&amp;", '&').replace("&quot;", '"')
        self.album = album.replace("&amp;", '&').replace("&quot;", '"')
        self.title = title.replace("&amp;", '&').replace("&quot;", '"')
        self.duration = int(duration)//1000
        self.location = location
        self.image = image
        self.genre = genre
        self.scrobble = False
        self.start = 0

class LastClient:
    api_key = "c1b1f995d8757b9873a353bc76807e97"
    secret = "f1132320d70c09790359c0fd4e98988a"
    def __init__(self, service, session = {}, user=None):
        self.service = service
        self.session = session
        if session.__contains__(1):
            self.oldScrobbler = OldScrobbler(user, session[1][1])

    def getToken(self):
        params = {
                    "api_key": self.api_key,
                    "method": "auth.gettoken",
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "get")
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            return (error, False)
        return (0, root.getElementsByTagName("token")[0].childNodes[0].toxml())

    def getTopTags(self, artist):
        params = {
                    "api_key": self.api_key,
                    "method": "artist.getTopTags",
                    "artist": artist,
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "get")
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            return (error, None)
        return (0, [t.childNodes[0].toxml().title() for t in root.getElementsByTagName("name")])

    def getSession(self, token):
        params = {
                    "api_key": self.api_key,
                    "method": "auth.getsession",
                    "token": token,
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "get") 
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            return (error, False, False)
        user = root.getElementsByTagName("name")[0].childNodes[0].toxml()
        session = root.getElementsByTagName("key")[0].childNodes[0].toxml()
        return (0, session, user)

    def _love(self, service, track):
        params = {
                    "api_key": self.api_key,
                    "method": "track.love",
                    "sk": self.session[service][1],
                    "artist": track.artist,
                    "track": track.title,
        }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "post", service)
        if not resp:
            return "-1"
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            return error
        return 0

    def love(self, track):
        e = dict()
        for service in self.session.keys():
            e[service] = self._love(service, track)
        return e

    def _updateNowPlaying(self, service, track):
        params = {
                    "api_key": self.api_key,
                    "method": "track.updateNowPlaying",
                    "sk": self.session[service][1],
                    "artist": track.artist,
                    "track": track.title,
                    "duration": track.duration,
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "post", service)
        if not resp:
            return "-1"
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            if error == "115":
                track.album = False
                self._updateNowPlaying(service, track)
            else:
                return error
        return 0

    def updateNowPlaying(self, track):
        e = dict()
        for service in self.session.keys():
            if service == 1: #FIXME: needed by Libre.fm until they implement Scrobbling 2.0 API
                e[service] = self.oldScrobbler.updateNowPlaying(track)
            else:
                e[service] = self._updateNowPlaying(service, track)
        return e

    def _scrobble(self, service, track):
        params = {
                    "artist": track.artist,
                    "track": track.title,
                    "timestamp": track.start,
                    "duration": track.duration,
                    "streamId": track.id,
                    "api_key": self.api_key,
                    "method": "track.scrobble",
                    "sk": self.session[service][1],
                }
        if track.album:
            params["album"] = track.album
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "post", service)
        if not resp:
            return "-1"
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            if error == "115":
                track.album = False
                self._scrobble(service, track)
            else:
                return error
        return 0
        
    def scrobble(self,track):
        e = dict()
        for service in self.session.keys():
            if service == 1: #FIXME: needed by Libre.fm until they implement Scrobbling 2.0 API
                e[service] = self.oldScrobbler.scrobble(track)
            else:
                e[service] = self._scrobble(service, track)
        return e
        
    def tuneIn(self, url):
        params = {
                    "api_key": self.api_key,
                    "method": "radio.tune",
                    "sk": self.session[self.service][1],
                    "station": url,
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "post")
        root = resp.documentElement
        status = root.attributes.item(0).value
        try:
            name = root.getElementsByTagName("name")[0].childNodes[0].toxml()
        except IndexError:
            name = None
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            return (error, name)
        return (0, name)

    def _isLoved(self, t, service):
        params = {
                    "api_key": self.api_key,
                    "method": "track.getInfo",
                    "artist": t.artist,
                    "track": t.title,
                    "username": self.session[service][0],
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "get", service)
        if not resp:
            return "-1"
        root = resp.documentElement
        status = root.attributes.item(0).value
        if not status == "ok":
            error = root.getElementsByTagName("error")[0].attributes.item(0).value
            if error == "3":
                return (True, 3)
            else:
                return (False, error)

        return (int(root.getElementsByTagName("userloved")[0].childNodes[0].toxml()), 0)
            
    def isLoved(self, t): # FIXME: errors handling
        e = dict()
        for service in self.session.keys():
            loved, e[service] = self._isLoved(t, service)
            if not loved:
                return False
        return True

    def getPlaylist(self):
        params = {
                    "api_key": self.api_key,
                    "method": "radio.getPlaylist",
                    "sk": self.session[self.service][1],
                    "bitrate": 128,
                    "speed_multiplier": 2.0,
                }
        params['api_sig'] = self._makeSign(params)
        resp = self._request(params, "get")
        if not resp:
            return ("-1", "FAILED", [])
        elif isinstance(resp, int):
            return (str(resp), "FAILED", [])
        try:
            root = resp.documentElement
            status = root.attributes.item(0).value
            if not status == "ok":
                error = root.getElementsByTagName("error")[0].attributes.item(0).value
                return (error, None, [])
        except IndexError:
            pass
        name = resp.getElementsByTagName("title")[0].childNodes[0].toxml()
        tracks = resp.getElementsByTagName("track")
        playlist = list()
        for t in tracks:
            if not t.getElementsByTagName("location")[0].hasChildNodes():
                continue
            location = t.getElementsByTagName("location")[0].childNodes[0].toxml()
            if not self.service == 0:
                try:
                    urllib.request.urlopen(location).read(5)
                except urllib.error.HTTPError as e:
                    #log.warning("HTTPError for %s (%s): %s" % (location, e.code, e.read()))
                    continue

            album = t.getElementsByTagName("album")[0]
            if album.hasChildNodes():
                album = album.childNodes[0].toxml()
            else:
                album = ""
            image = t.getElementsByTagName("image")[0]
            if image.hasChildNodes():
                image = image.childNodes[0].toxml()
            else:
                image = ""
            playlist.append(
                            Track(
                                    t.getElementsByTagName("identifier")[0].childNodes[0].toxml(),
                                    t.getElementsByTagName("creator")[0].childNodes[0].toxml(),
                                    album,
                                    t.getElementsByTagName("title")[0].childNodes[0].toxml(),
                                    t.getElementsByTagName("duration")[0].childNodes[0].toxml(),
                                    location,
                                    image,
                                )
                            )
        if playlist == []:
            return("-1", name, playlist)
        return (0, name, playlist)

    def _makeSign(self, params):
        p = list()
        for k, v in list(params.items()):
            p.append(str(k)+str(v))
        p.sort()
        p = "".join(p)
        return md5(bytes(p+self.secret, "utf-8")).hexdigest()

    def _request(self, data, type, service=False, debug=False):
        if not service and isinstance(service, bool):
            service = self.service
        if debug:
            print(services[service][1] + urllib.parse.urlencode(data))
        try:
            if type == 'get':
                resp = urllib.request.urlopen(services[service][1] + urllib.parse.urlencode(data)).read()
            else:
                resp = urllib.request.urlopen(services[service][1], urllib.parse.urlencode(data).encode("utf-8")).read()
        except urllib.error.HTTPError as e:
            resp = e.read()
            if e.code in (403, 400, 503):
                pass
            else:
                raise
        if debug:
            print(str(resp, "utf8"))
        if resp == b'BADSESSION\n':
            return 9
        elif resp == b'FAILED\n':
            return False
        else:
            try:
                return minidom.parseString(resp)
            except:
                print(resp)
                return False

class OldScrobbler(LastClient): #FIXME: needed by Libre.fm until they implement Scrobbling 2.0 API
    def __init__(self, user, wssession):
        if not user:
            self = None
            return None
        self.user = user
        self.wssession = wssession
        self.session = False
        self.urls = []

    def updateNowPlaying(self, t):
        if not self.urls:
            e = self.handshake()
            if not self.urls:
                return e
            
        params = {
                    "s": self.session,
                    "a": t.artist,
                    "t": t.title,
                    "l": t.duration,
                    "n": "",
                    "m": "",
                    "b": "",
                }
        if t.album:
            params["b"] = t.album

        resp = self._request(self.urls[0], params, "post")
        if not resp == b"OK\n":
            print(resp)
            return "-1"
        return 0

    def scrobble(self, t):
        if not self.urls:
            e = self.handshake()
            if not self.urls:
                return e

        params = {
                    "s": self.session,
                    "a[0]": t.artist,
                    "t[0]": t.title,
                    "i[0]": t.start,
                    "o[0]": "P",
                    "r[0]": "",
                    "l[0]": t.duration,
                    "n[0]": "",
                    "m[0]": "",
                    "b[0]": "",
                }
        if t.album:
            params["b[0]"] = t.album
        resp = self._request(self.urls[1], params, "post")
        if not resp == b"OK\n":
            print(resp)
            return "-1"
        return 0
    
    def handshake(self):
        t = int(time.time())
        auth = md5(bytes(self.user[1] + str(t), "utf8")).hexdigest()
        params = {
                    "hs": "true",
                    "p": "1.2.1",
                    "c": "pyl",
                    "v": "1.0",
                    "u": self.user[0],
                    "t": t,
                    "a": auth,
                    "ws": self.session,
                }
        resp = str(self._request("http://turtle.libre.fm/?", params, "get"), "utf8").split("\n")
        if resp[0] == "OK":
            self.session = resp[1]
            self.urls = [resp[2],resp[3]]
            return True
        else:
            return False
        
    def _request(self, url, data, type):
        try:
            if type == 'get':
                resp = urllib.request.urlopen(url + urllib.parse.urlencode(data)).read()
            else:
                resp = urllib.request.urlopen(url, urllib.parse.urlencode(data).encode("utf-8")).read()
        except urllib.error.HTTPError as e:
            resp = e.read()
            if e.code in (403, 400, 503):
                pass
            elif e.code in (500,):
                return e.code
            else:
                raise
        if resp == b'BADSESSION\n':
            return 9
        elif resp == b'FAILED\n':
            return False
        else:
            return resp
